package paint;




/**
 * This class provides an immutable 4d matrix, and methods to manipulate a vector3D
 * @author DustinS
 *
 */
public class Matrix {
	private static final int ROWS = 4;
	private static final int COLS = 4;
	
	private double[][] mMatrix;
	
	private Matrix(double[][] matrix) {
		mMatrix = matrix;
	}
	
	private Matrix(Vector4D u, Vector4D v, Vector4D n, Vector4D t) {
		double[][] matrix = new double[ROWS][COLS];
		
		matrix[0][0] = u.getX();
		matrix[0][1] = u.getY();
		matrix[0][2] = u.getZ();
		matrix[0][3] = u.getW();
		
		matrix[1][0] = v.getX();
		matrix[1][1] = v.getY();
		matrix[1][2] = v.getZ();
		matrix[1][3] = v.getW();
		
		matrix[2][0] = n.getX();
		matrix[2][1] = n.getY();
		matrix[2][2] = n.getZ();
		matrix[2][3] = n.getW();
		
		matrix[3][0] = t.getX();
		matrix[3][1] = t.getY();
		matrix[3][2] = t.getZ();
		matrix[3][3] = t.getW();
		
		mMatrix = matrix;
	}
	
	/**
	 * @return the identity matrix
	 */
	public static Matrix identityMatrix() {
		double[][] identity = {	{1,0,0,0},
								{0,1,0,0},
								{0,0,1,0},
								{0,0,0,1}};
		return new Matrix(identity);
	}
	
	/**
	 * Perform a rotation about the x axis on the given matrix
	 * @param original the matrix to perform the rotation on
	 * @param theta the number of degrees to rotate
	 * @return the transformed matrix
	 */
	public static Matrix rotateX(Matrix original, double theta) {
		theta = Math.toRadians(theta);
		double[][] rotationX = {	{1,0,				0,				0},
									{0,Math.cos(theta),-Math.sin(theta),0},
									{0,Math.sin(theta),Math.cos(theta),	0},
									{0,0,				0,				1}};
		return multiplyMatrices(original, rotationX);
	}
	
	/**
	 * Perform a rotation about the y axis on the given matrix
	 * @param original the matrix to perform the rotation on
	 * @param theta the number of degrees to rotate
	 * @return the transformed matrix
	 */
	public static Matrix rotateY(Matrix original, double theta) {
		theta = Math.toRadians(theta);
		double[][] rotationY = {	{Math.cos(theta),0,	Math.sin(theta),0},
									{0,				 1,  0,				0},
									{-Math.sin(theta),0,Math.cos(theta),0},
									{0,				 0,  0,				1}};
		return multiplyMatrices(original, rotationY);
	}
	
	/**
	 * Perform a rotation about the z axis on the given matrix
	 * @param original the matrix to perform the rotation on
	 * @param theta the number of degrees to rotate
	 * @return the transformed matrix
	 */
	public static Matrix rotateZ(Matrix original, double theta) {
		theta = Math.toRadians(theta);
		double[][] rotationZ = {	{Math.cos(theta),-Math.sin(theta),0,0},
									{Math.sin(theta),Math.cos(theta), 0,0},
									{0,				 0,				  1,0},
									{0,				 0,			 	  0,1}};
		return multiplyMatrices(original, rotationZ);
	}
	
	/**
	 * perform a translation at the end of the previous operations defined in the matrix
	 * @param original the matrix to perform the transformation on
	 * @param x
	 * @param y
	 * @param z
	 * @return the result of the transformation
	 */
	public static Matrix translate(Matrix original, double x, double y, double z) {
		double[][] translation = {	{1,0,0,x},
									{0,1,0,y},
									{0,0,1,z},
									{0,0,0,1}};
		return multiplyMatrices(original, translation);
	}
	
	/**
	 * Performs the translation before any other operations 
	 * @param original
	 * @param x
	 * @param y
	 * @param z
	 * @return
	 */
	public static Matrix preTranslate(Matrix original, double x, double y, double z) {
		double[][] translation = {	{1,0,0,x},
									{0,1,0,y},
									{0,0,1,z},
									{0,0,0,1}};
		return multiplyMatrices(new Matrix(translation), original.mMatrix);
	}
	
	/**
	 * Perform a scale transformation
	 * @param original the matrix to perform the scale on
	 * @param x
	 * @param y
	 * @param z
	 * @return the result of the transformation
	 */
	public static Matrix scale(Matrix original, double x, double y, double z) {
		double[][] scale = {		{x,0,0,0},
									{0,y,0,0},
									{0,0,z,0},
									{0,0,0,1}};
		return multiplyMatrices(original, scale);
	}
	
	public static Vector4D multiply(Matrix tm, Vector3D vector) {
		return multiply(tm, new Vector4D(vector));
	}
	
	public static Vector4D multiply(Matrix tm, Vector4D vector) {
		double[] result = new double[ROWS];
		for(int row = 0; row < ROWS; row++) {
			result[row] = 0;
			for(int col = 0; col < COLS; col++) {
				result[row] += tm.mMatrix[row][col] * vector.vector[col];
			}
		}
		return new Vector4D(result);
	}
	
	public static Matrix lookAt() {
		return Matrix.identityMatrix();
	}
	
	public static Matrix lookAt(double eyeX, double eyeY, double eyeZ,
								double atX,  double atY,  double atZ,
								double upX,  double upY,  double upZ) {
		Vector4D eye = new Vector4D(eyeX, eyeY, eyeZ,1.0);
		Vector4D at = new Vector4D(atX, atY, atZ,1.0);
		Vector4D up = new Vector4D(upX, upY, upZ,0.0);
		
		Vector4D n = Vector4D.normalize(Vector4D.subtract(eye, at));
		Vector4D u = Vector4D.normalize(Vector4D.cross(up, n));		
		Vector4D v = Vector4D.normalize(Vector4D.cross(n, u));
		Vector4D t = new Vector4D(0, 0, 0, 1);
		
		Matrix c = new Matrix(u, v, n, t);
		Matrix translate = translate(identityMatrix(), -eye.getX(), -eye.getY(), -eye.getZ());
		Matrix result = multiplyMatrices(translate, c);
		return result;
	}
	
	public static Matrix ortho() {
		return Matrix.identityMatrix();
	}
	
	public static Matrix ortho(double left, double right, double bottom, double top,
								double zNear, double zFar) {
		
		double[][] c = new double[ROWS][COLS];
	    c[0][0] = 2.0/(right - left);
	    c[1][1] = 2.0/(top - bottom);
	    c[2][2] = 2.0/(zNear - zFar);
	    c[3][3] = 1.0;
	    c[0][3] = -(right + left)/(right - left);
	    c[1][3] = -(top + bottom)/(top - bottom);
	    c[2][3] = -(zFar + zNear)/(zFar - zNear);    
	    return new Matrix(c);
	}
	
	public static Matrix frustum() {
		return Matrix.identityMatrix();
	}
	
	public static Matrix frustum(double left, double right, double bottom, double top,
								  double zNear, double zFar) {
		double[][] c = new double[ROWS][COLS];
	    c[0][0] = 2.0*zNear/(right - left);
	    c[0][2] = (right + left)/(right - left);
	    c[1][1] = 2.0*zNear/(top - bottom);
	    c[1][2] = (top + bottom)/(top - bottom);
	    c[2][2] = -(zFar + zNear)/(zFar - zNear);
	    c[2][3] = -2.0*zFar*zNear/(zFar - zNear);
	    c[3][2] = -1.0;
//	    for(int i = 0; i < c.length; i++){
//	    	for(int j = 0; j < c[0].length; j++) {
//	    		System.out.print(c[i][j] +", ");
//	    	}
//	    	System.out.println();
//	    }
//    	System.out.println();
	    
	    return new Matrix(c);
	}

	
	/**
	 * @param vector the vector to project
	 * @return a 4D vector that has been projected onto the xy plane (drop the z value)
	 */
//	public static Vector4D projectXY(Vector3D vector) {
//		double[][] projectionXY = {	{1,0,0,0},
//									{0,1,0,0},
//									{0,0,0,0},
//									{0,0,0,1}};
//		return multiply(new Matrix(projectionXY), vector);
//	}
	
	public static Matrix multiplyMatrices(Matrix original, Matrix transformation) {
		return multiplyMatrices(original, transformation.mMatrix);
	}
	
	/**
	 * Multiply two 4x4 matrices
	 * @param original the matrix to apply the transformation to
	 * @param transformation the transformation to apply
	 * @return the result of transformation * original
	 */
	public static Matrix multiplyMatrices(Matrix original, double[][] transformation) {
		double[][] result = new double[ROWS][COLS];
		for(int row = 0; row < ROWS; row++) {//rows in transformation matrix
			for(int col = 0; col < COLS; col++) {//cols in transformation matrix
				result[row][col] = 0;
				for(int index = 0; index < ROWS; index++) {
					result[row][col] = result[row][col] + transformation[row][index] * original.mMatrix[index][col];
				}
			}
		}		
		return new Matrix(result);
	}
	
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		for(int row = 0; row < ROWS; row++) {
			for(int col = 0; col < COLS; col++) {
				sb.append(mMatrix[row][col]);
				sb.append(" ");
			}
			sb.append("\n");
		}
		return sb.toString();
	}
}
